package gitbucket.core.service

import javax.servlet.http.HttpServletRequest
import com.nimbusds.jose.JWSAlgorithm
import com.nimbusds.oauth2.sdk.auth.Secret
import com.nimbusds.oauth2.sdk.id.{ClientID, Issuer}
import gitbucket.core.service.SystemSettingsService.{getOptionValue, _}
import gitbucket.core.util.ConfigUtil._
import gitbucket.core.util.Directory._

import scala.util.Using

trait SystemSettingsService {

    def baseUrl(implicit request: HttpServletRequest): String = loadSystemSettings()
        .baseUrl(request)

    def saveSystemSettings(settings: SystemSettings): Unit = {
        val props = new java.util.Properties()
        settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
        settings.information.foreach(x => props.setProperty(Information, x))
        props.setProperty(
          AllowAccountRegistration,
          settings.basicBehavior.allowAccountRegistration.toString
        )
        props.setProperty(AllowResetPassword, settings.basicBehavior.allowResetPassword.toString)
        props
            .setProperty(AllowAnonymousAccess, settings.basicBehavior.allowAnonymousAccess.toString)
        props.setProperty(
          IsCreateRepoOptionPublic,
          settings.basicBehavior.isCreateRepoOptionPublic.toString
        )
        props.setProperty(
          RepositoryOperationCreate,
          settings.basicBehavior.repositoryOperation.create.toString
        )
        props.setProperty(
          RepositoryOperationDelete,
          settings.basicBehavior.repositoryOperation.delete.toString
        )
        props.setProperty(
          RepositoryOperationRename,
          settings.basicBehavior.repositoryOperation.rename.toString
        )
        props.setProperty(
          RepositoryOperationTransfer,
          settings.basicBehavior.repositoryOperation.transfer.toString
        )
        props.setProperty(
          RepositoryOperationFork,
          settings.basicBehavior.repositoryOperation.fork.toString
        )
        props.setProperty(Gravatar, settings.basicBehavior.gravatar.toString)
        props.setProperty(Notification, settings.basicBehavior.notification.toString)
        props.setProperty(
          LimitVisibleRepositories,
          settings.basicBehavior.limitVisibleRepositories.toString
        )
        props.setProperty(SshEnabled, settings.ssh.enabled.toString)
        settings.ssh.bindAddress.foreach { bindAddress =>
            props.setProperty(SshBindAddressHost, bindAddress.host.trim())
            props.setProperty(SshBindAddressPort, bindAddress.port.toString)
        }
        settings.ssh.publicAddress.foreach { publicAddress =>
            props.setProperty(SshPublicAddressHost, publicAddress.host.trim())
            props.setProperty(SshPublicAddressPort, publicAddress.port.toString)
        }
        props.setProperty(UseSMTP, settings.useSMTP.toString)
        if (settings.useSMTP) {
            settings.smtp.foreach { smtp =>
                props.setProperty(SmtpHost, smtp.host)
                smtp.port.foreach(x => props.setProperty(SmtpPort, x.toString))
                smtp.user.foreach(props.setProperty(SmtpUser, _))
                smtp.password.foreach(props.setProperty(SmtpPassword, _))
                smtp.ssl.foreach(x => props.setProperty(SmtpSsl, x.toString))
                smtp.starttls.foreach(x => props.setProperty(SmtpStarttls, x.toString))
                smtp.fromAddress.foreach(props.setProperty(SmtpFromAddress, _))
                smtp.fromName.foreach(props.setProperty(SmtpFromName, _))
            }
        }
        props.setProperty(LdapAuthentication, settings.ldapAuthentication.toString)
        if (settings.ldapAuthentication) {
            settings.ldap.foreach { ldap =>
                props.setProperty(LdapHost, ldap.host)
                ldap.port.foreach(x => props.setProperty(LdapPort, x.toString))
                ldap.bindDN.foreach(x => props.setProperty(LdapBindDN, x))
                ldap.bindPassword.foreach(x => props.setProperty(LdapBindPassword, x))
                props.setProperty(LdapBaseDN, ldap.baseDN)
                props.setProperty(LdapUserNameAttribute, ldap.userNameAttribute)
                ldap.additionalFilterCondition
                    .foreach(x => props.setProperty(LdapAdditionalFilterCondition, x))
                ldap.fullNameAttribute.foreach(x => props.setProperty(LdapFullNameAttribute, x))
                ldap.mailAttribute.foreach(x => props.setProperty(LdapMailAddressAttribute, x))
                ldap.tls.foreach(x => props.setProperty(LdapTls, x.toString))
                ldap.ssl.foreach(x => props.setProperty(LdapSsl, x.toString))
                ldap.keystore.foreach(x => props.setProperty(LdapKeystore, x))
            }
        }
        props.setProperty(OidcAuthentication, settings.oidcAuthentication.toString)
        if (settings.oidcAuthentication) {
            settings.oidc.foreach { oidc =>
                props.setProperty(OidcIssuer, oidc.issuer.getValue)
                props.setProperty(OidcClientId, oidc.clientID.getValue)
                props.setProperty(OidcClientSecret, oidc.clientSecret.getValue)
                oidc.jwsAlgorithm.foreach { x => props.setProperty(OidcJwsAlgorithm, x.getName) }
            }
        }
        props.setProperty(SkinName, settings.skinName)
        settings.userDefinedCss.foreach(x => props.setProperty(UserDefinedCss, x))
        props.setProperty(ShowMailAddress, settings.showMailAddress.toString)
        props.setProperty(WebHookBlockPrivateAddress, settings.webHook.blockPrivateAddress.toString)
        props.setProperty(WebHookWhitelist, settings.webHook.whitelist.mkString("\n"))
        props.setProperty(UploadMaxFileSize, settings.upload.maxFileSize.toString)
        props.setProperty(UploadTimeout, settings.upload.timeout.toString)
        props.setProperty(UploadLargeMaxFileSize, settings.upload.largeMaxFileSize.toString)
        props.setProperty(UploadLargeTimeout, settings.upload.largeTimeout.toString)
        props.setProperty(RepositoryViewerMaxFiles, settings.repositoryViewer.maxFiles.toString)
        props.setProperty(
          RepositoryViewerMaxDiffFiles,
          settings.repositoryViewer.maxDiffFiles.toString
        )
        props.setProperty(
          RepositoryViewerMaxDiffLines,
          settings.repositoryViewer.maxDiffLines.toString
        )
        props.setProperty(DefaultBranch, settings.defaultBranch)

        Using.resource(new java.io.FileOutputStream(GitBucketConf)) { out =>
            props.store(out, null)
        }
    }

    def loadSystemSettings(): SystemSettings = {
        val props = new java.util.Properties()
        if (GitBucketConf.exists) {
            Using.resource(new java.io.FileInputStream(GitBucketConf)) { in => props.load(in) }
        }
        loadSystemSettings(props)
    }

    def loadSystemSettings(props: java.util.Properties): SystemSettings = {
        SystemSettings(
          getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
          getOptionValue(props, Information, None),
          BasicBehavior(
            getValue(props, AllowAccountRegistration, false),
            getValue(props, AllowResetPassword, false),
            getValue(props, AllowAnonymousAccess, true),
            getValue(props, IsCreateRepoOptionPublic, true),
            RepositoryOperation(
              create = getValue(props, RepositoryOperationCreate, true),
              delete = getValue(props, RepositoryOperationDelete, true),
              rename = getValue(props, RepositoryOperationRename, true),
              transfer = getValue(props, RepositoryOperationTransfer, true),
              fork = getValue(props, RepositoryOperationFork, true)
            ),
            getValue(props, Gravatar, false),
            getValue(props, Notification, false),
            getValue(props, LimitVisibleRepositories, false)
          ),
          Ssh(
            enabled = getValue(props, SshEnabled, false),
            bindAddress = {
                // try the new-style configuration first
                getOptionValue[String](props, SshBindAddressHost, None).map(h =>
                    SshAddress(
                      h,
                      getValue(props, SshBindAddressPort, DefaultSshPort),
                      GenericSshUser
                    )
                ).orElse(
                  // otherwise try to get old-style configuration
                  getOptionValue[String](props, SshHost, None).map(_.trim).map(h =>
                      SshAddress(h, getValue(props, SshPort, DefaultSshPort), GenericSshUser)
                  )
                )
            },
            publicAddress = getOptionValue[String](props, SshPublicAddressHost, None).map(h =>
                SshAddress(h, getValue(props, SshPublicAddressPort, PublicSshPort), GenericSshUser)
            )
          ),
          getValue(
            props,
            UseSMTP,
            getValue(props, Notification, false)
          ), // handle migration scenario from only notification to useSMTP
          if (getValue(props, UseSMTP, getValue(props, Notification, false))) {
              Some(Smtp(
                getValue(props, SmtpHost, ""),
                getOptionValue(props, SmtpPort, Some(DefaultSmtpPort)),
                getOptionValue(props, SmtpUser, None),
                getOptionValue(props, SmtpPassword, None),
                getOptionValue[Boolean](props, SmtpSsl, None),
                getOptionValue[Boolean](props, SmtpStarttls, None),
                getOptionValue(props, SmtpFromAddress, None),
                getOptionValue(props, SmtpFromName, None)
              ))
          } else None,
          getValue(props, LdapAuthentication, false),
          if (getValue(props, LdapAuthentication, false)) {
              Some(Ldap(
                getValue(props, LdapHost, ""),
                getOptionValue(props, LdapPort, Some(DefaultLdapPort)),
                getOptionValue(props, LdapBindDN, None),
                getOptionValue(props, LdapBindPassword, None),
                getValue(props, LdapBaseDN, ""),
                getValue(props, LdapUserNameAttribute, ""),
                getOptionValue(props, LdapAdditionalFilterCondition, None),
                getOptionValue(props, LdapFullNameAttribute, None),
                getOptionValue(props, LdapMailAddressAttribute, None),
                getOptionValue[Boolean](props, LdapTls, None),
                getOptionValue[Boolean](props, LdapSsl, None),
                getOptionValue(props, LdapKeystore, None)
              ))
          } else None,
          getValue(props, OidcAuthentication, false),
          if (getValue(props, OidcAuthentication, false)) {
              Some(OIDC(
                getValue(props, OidcIssuer, ""),
                getValue(props, OidcClientId, ""),
                getValue(props, OidcClientSecret, ""),
                getOptionValue(props, OidcJwsAlgorithm, None)
              ))
          } else { None },
          getValue(props, SkinName, "skin-blue"),
          getOptionValue(props, UserDefinedCss, None),
          getValue(props, ShowMailAddress, false),
          WebHook(
            getValue(props, WebHookBlockPrivateAddress, false),
            getSeqValue(props, WebHookWhitelist, "")
          ),
          Upload(
            getValue(props, UploadMaxFileSize, 3 * 1024 * 1024),
            getValue(props, UploadTimeout, 3 * 10000),
            getValue(props, UploadLargeMaxFileSize, 3 * 1024 * 1024),
            getValue(props, UploadLargeTimeout, 3 * 10000)
          ),
          RepositoryViewerSettings(
            getValue(props, RepositoryViewerMaxFiles, 0),
            getValue(props, RepositoryViewerMaxDiffFiles, 100),
            getValue(props, RepositoryViewerMaxDiffLines, 1000)
          ),
          getValue(props, DefaultBranch, "main")
        )
    }
}

object SystemSettingsService {
    import scala.reflect.ClassTag

    private val HttpProtocols = Vector("http", "https")

    case class SystemSettings(
        baseUrl: Option[String],
        information: Option[String],
        basicBehavior: BasicBehavior,
        ssh: Ssh,
        useSMTP: Boolean,
        smtp: Option[Smtp],
        ldapAuthentication: Boolean,
        ldap: Option[Ldap],
        oidcAuthentication: Boolean,
        oidc: Option[OIDC],
        skinName: String,
        userDefinedCss: Option[String],
        showMailAddress: Boolean,
        webHook: WebHook,
        upload: Upload,
        repositoryViewer: RepositoryViewerSettings,
        defaultBranch: String
    ) {
        def baseUrl(request: HttpServletRequest): String = baseUrl.getOrElse(parseBaseUrl(request))
            .stripSuffix("/")

        def parseBaseUrl(req: HttpServletRequest): String = {
            val url = req.getRequestURL.toString
            val path = req.getRequestURI
            val contextPath = req.getContextPath
            val len = url.length - path.length + contextPath.length

            val base = url.substring(0, len).stripSuffix("/")
            Option(req.getHeader("X-Forwarded-Proto")).map(_.toLowerCase())
                .filter(HttpProtocols.contains).fold(base)(_ + base.dropWhile(_ != ':'))
        }

        def sshBindAddress: Option[SshAddress] = ssh.bindAddress

        def sshPublicAddress: Option[SshAddress] = ssh.publicAddress.orElse(ssh.bindAddress)

        def sshUrl: Option[String] = ssh.getUrl

        def sshUrl(owner: String, name: String): Option[String] = ssh
            .getUrl(owner: String, name: String)
    }

    case class BasicBehavior(
        allowAccountRegistration: Boolean,
        allowResetPassword: Boolean,
        allowAnonymousAccess: Boolean,
        isCreateRepoOptionPublic: Boolean,
        repositoryOperation: RepositoryOperation,
        gravatar: Boolean,
        notification: Boolean,
        limitVisibleRepositories: Boolean
    )

    case class RepositoryOperation(
        create: Boolean,
        delete: Boolean,
        rename: Boolean,
        transfer: Boolean,
        fork: Boolean
    )

    case class Ssh(
        enabled: Boolean,
        bindAddress: Option[SshAddress],
        publicAddress: Option[SshAddress]
    ) {

        def getUrl: Option[String] =
            if (enabled) { publicAddress.map(_.getUrl).orElse(bindAddress.map(_.getUrl)) }
            else { None }

        def getUrl(owner: String, name: String): Option[String] =
            if (enabled) {
                publicAddress.map(_.getUrl(owner, name))
                    .orElse(bindAddress.map(_.getUrl(owner, name)))
            } else { None }
    }

    object Ssh {
        def apply(
            enabled: Boolean,
            bindAddress: Option[SshAddress],
            publicAddress: Option[SshAddress]
        ): Ssh = new Ssh(enabled, bindAddress, publicAddress.orElse(bindAddress))
    }

    case class Ldap(
        host: String,
        port: Option[Int],
        bindDN: Option[String],
        bindPassword: Option[String],
        baseDN: String,
        userNameAttribute: String,
        additionalFilterCondition: Option[String],
        fullNameAttribute: Option[String],
        mailAttribute: Option[String],
        tls: Option[Boolean],
        ssl: Option[Boolean],
        keystore: Option[String]
    )

    case class OIDC(
        issuer: Issuer,
        clientID: ClientID,
        clientSecret: Secret,
        jwsAlgorithm: Option[JWSAlgorithm]
    )
    object OIDC {
        def apply(
            issuer: String,
            clientID: String,
            clientSecret: String,
            jwsAlgorithm: Option[String]
        ): OIDC = new OIDC(
          new Issuer(issuer),
          new ClientID(clientID),
          new Secret(clientSecret),
          jwsAlgorithm.map(JWSAlgorithm.parse)
        )
    }

    case class Smtp(
        host: String,
        port: Option[Int],
        user: Option[String],
        password: Option[String],
        ssl: Option[Boolean],
        starttls: Option[Boolean],
        fromAddress: Option[String],
        fromName: Option[String]
    )

    case class Proxy(host: String, port: Int, user: Option[String], password: Option[String])

    case class SshAddress(host: String, port: Int, genericUser: String) {

        def isDefaultPort: Boolean = port == PublicSshPort

        def getUrl: String =
            if (isDefaultPort) { s"${genericUser}@${host}" }
            else { s"${genericUser}@${host}:${port}" }

        def getUrl(owner: String, name: String): String =
            if (isDefaultPort) { s"${genericUser}@${host}:${owner}/${name}.git" }
            else { s"ssh://${genericUser}@${host}:${port}/${owner}/${name}.git" }
    }

    case class WebHook(blockPrivateAddress: Boolean, whitelist: Seq[String])

    case class Upload(maxFileSize: Long, timeout: Long, largeMaxFileSize: Long, largeTimeout: Long)

    case class RepositoryViewerSettings(maxFiles: Int, maxDiffFiles: Int, maxDiffLines: Int)

    val GenericSshUser = "git"
    val PublicSshPort = 22
    val DefaultSshPort = 29418
    val DefaultSmtpPort = 25
    val DefaultLdapPort = 389

    private val BaseURL = "base_url"
    private val Information = "information"
    private val AllowAccountRegistration = "allow_account_registration"
    private val AllowResetPassword = "allow_reset_password"
    private val AllowAnonymousAccess = "allow_anonymous_access"
    private val IsCreateRepoOptionPublic = "is_create_repository_option_public"
    private val RepositoryOperationCreate = "repository_operation_create"
    private val RepositoryOperationDelete = "repository_operation_delete"
    private val RepositoryOperationRename = "repository_operation_rename"
    private val RepositoryOperationTransfer = "repository_operation_transfer"
    private val RepositoryOperationFork = "repository_operation_fork"
    private val Gravatar = "gravatar"
    private val Notification = "notification"
    private val LimitVisibleRepositories = "limitVisibleRepositories"
    private val SshEnabled = "ssh"
    private val SshHost = "ssh.host"
    private val SshPort = "ssh.port"
    private val SshBindAddressHost = "ssh.bindAddress.host"
    private val SshBindAddressPort = "ssh.bindAddress.port"
    private val SshPublicAddressHost = "ssh.publicAddress.host"
    private val SshPublicAddressPort = "ssh.publicAddress.port"
    private val UseSMTP = "useSMTP"
    private val SmtpHost = "smtp.host"
    private val SmtpPort = "smtp.port"
    private val SmtpUser = "smtp.user"
    private val SmtpPassword = "smtp.password"
    private val SmtpSsl = "smtp.ssl"
    private val SmtpStarttls = "smtp.starttls"
    private val SmtpFromAddress = "smtp.from_address"
    private val SmtpFromName = "smtp.from_name"
    private val LdapAuthentication = "ldap_authentication"
    private val LdapHost = "ldap.host"
    private val LdapPort = "ldap.port"
    private val LdapBindDN = "ldap.bindDN"
    private val LdapBindPassword = "ldap.bind_password"
    private val LdapBaseDN = "ldap.baseDN"
    private val LdapUserNameAttribute = "ldap.username_attribute"
    private val LdapAdditionalFilterCondition = "ldap.additional_filter_condition"
    private val LdapFullNameAttribute = "ldap.fullname_attribute"
    private val LdapMailAddressAttribute = "ldap.mail_attribute"
    private val LdapTls = "ldap.tls"
    private val LdapSsl = "ldap.ssl"
    private val LdapKeystore = "ldap.keystore"
    private val OidcAuthentication = "oidc_authentication"
    private val OidcIssuer = "oidc.issuer"
    private val OidcClientId = "oidc.client_id"
    private val OidcClientSecret = "oidc.client_secret"
    private val OidcJwsAlgorithm = "oidc.jws_algorithm"
    private val SkinName = "skinName"
    private val UserDefinedCss = "userDefinedCss"
    private val ShowMailAddress = "showMailAddress"
    private val WebHookBlockPrivateAddress = "webhook.block_private_address"
    private val WebHookWhitelist = "webhook.whitelist"
    private val UploadMaxFileSize = "upload.maxFileSize"
    private val UploadTimeout = "upload.timeout"
    private val UploadLargeMaxFileSize = "upload.largeMaxFileSize"
    private val UploadLargeTimeout = "upload.largeTimeout"
    private val RepositoryViewerMaxFiles = "repository_viewer_max_files"
    private val RepositoryViewerMaxDiffFiles = "repository_viewer_max_diff_files"
    private val RepositoryViewerMaxDiffLines = "repository_viewer_max_diff_lines"
    private val DefaultBranch = "default_branch"

    private def getValue[A: ClassTag](props: java.util.Properties, key: String, default: A): A = {
        getConfigValue(key).getOrElse {
            val value = props.getProperty(key)
            if (value == null || value.isEmpty) { default }
            else { convertType(value).asInstanceOf[A] }
        }
    }

    private def getSeqValue[A: ClassTag](
        props: java.util.Properties,
        key: String,
        default: A
    ): Seq[A] = {
        getValue[String](props, key, "").split("\n").toIndexedSeq.map { value =>
            if (value == null || value.isEmpty) { default }
            else { convertType(value).asInstanceOf[A] }
        }
    }

    private def getOptionValue[A: ClassTag](
        props: java.util.Properties,
        key: String,
        default: Option[A]
    ): Option[A] = {
        getConfigValue(key).orElse {
            val value = props.getProperty(key)
            if (value == null || value.isEmpty) { default }
            else { Some(convertType(value)).asInstanceOf[Option[A]] }
        }
    }

}
